﻿using Letter.IO.Scheduler;
using System;
using System.Buffers;
using ePipe;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using eLibrary.Environment;
using Letter.IO.Memorizer;
using Letter.Net.Session;

namespace Letter.Net.Udp;

sealed class UdpHost : AHost<UdpTransportOptions, UdpHostFeatureOptions>, IUdpHost
{
    private static MemoryPool<byte> memoryPool = MemoryPoolProvider.ShareMemoryPool<byte>();
    
    private static ISession PooledSnderTransport(Socket socket, IChannelSettings settings, AConnection connection)
    {
        var setting = new TransportSettings(
            settings.Scheduler, 
            settings.MemoryPool,
            settings.InputOptions,
            settings.OutputOptions
        );
        
        return SessionProvider.CreateUdpSessionFromSenderPool(socket, setting, connection);
    }

    private static ISession SolelySnderTransport(Socket socket, IChannelSettings settings, AConnection connection)
    {
        var setting = new TransportSettings(
            settings.Scheduler, 
            settings.MemoryPool,
            settings.InputOptions,
            settings.OutputOptions
        );
        
        return SessionProvider.CreateUdpSessionFromSenderSole(socket, setting, connection);
    }

    public UdpHost(string name) : base(name)
    {
    }

    private IChannelSettings setting;
    private EndPoint localAddress;

    private Socket socket;
    private ISession channel;
    


    public override EndPoint BindEndPoint
    {
        get { return this.localAddress; }
    }

    public override void Build()
    {
        base.Build();

        var maxReadBufferSize = (this.transportOptions.MaxReadBufferSize == null) ? 0 : this.transportOptions.MaxReadBufferSize.Value;
        var maxWriteBufferSize = (this.transportOptions.MaxWriteBufferSize == null) ? 0 : this.transportOptions.MaxWriteBufferSize.Value;
        var applicationScheduler = PipeScheduler.ThreadPool;
        var transportScheduler = SchedulerUtil.SelectSocketsScheduler();
        this.setting = new ChannelSettings()
        {
            MemoryPool = memoryPool,
            Scheduler = transportScheduler,
            InputOptions = new PipeOptions<byte>(memoryPool, applicationScheduler, transportScheduler, maxReadBufferSize, maxReadBufferSize / 2, useSynchronizationContext: false),
            OutputOptions = new PipeOptions<byte>(memoryPool, transportScheduler, applicationScheduler, maxWriteBufferSize, maxWriteBufferSize / 2, useSynchronizationContext: false),
        };
    }

    public override async Task BindAsync(EndPoint endPoint)
    {
        var socket = new Socket(endPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
        this.SocketControl(socket);
        try
        {
            socket.Bind(endPoint);
        }
        catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
        {
            throw;
        }

        UdpHostFeatureSetting.SettingFeature(socket, this.featureOptions);
        var connection = this.CreateConnection();

        ISession transmitter;
        if (this.transportOptions.PooledSnder)
            transmitter = PooledSnderTransport(socket, this.setting, connection);
        else
            transmitter = SolelySnderTransport(socket, this.setting, connection);
        await transmitter.StartAsync();
        
        this.channel = transmitter;
        
        this.localAddress = socket.LocalEndPoint;
        this.socket = socket;
    }
    
    private void SocketControl(Socket socket)
    {
        if (OS.IsWindows())
        {
            uint IOC_IN = 0x80000000;
            uint IOC_VENDOR = 0x18000000;
            uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
            socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
        }
    }
    
    public override async Task ConnectAsync(EndPoint endPoint)
    {
        if (this.socket == null)
        {
            throw new NullReferenceException("The socket must be bind first");
        }

        try
        {
            await this.socket.ConnectAsync(endPoint);
        }
        catch (Exception)
        {
            throw;
        }
        
        ((IUdpConnected)this.channel).Connecte();
    }

    public override async Task StopAsync()
    {
        await this.channel.CloseAsync();
    }
}